package com.smartalarmclock.Model;

import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import com.smartalarmclock.Model.Reference.DeleteMethod;
import com.smartalarmclock.Model.Exceptions.DataEntityDoesNotExistException;
import com.smartalarmclock.Model.Exceptions.InvalidDeleteDataEntityOperationException;
import com.smartalarmclock.Model.Exceptions.InvalidNullReferenceOnDeleteException;
import com.smartalarmclock.Model.Exceptions.InvalidReferenceToRemoveException;
import com.smartalarmclock.Model.Exceptions.InvalidSaveDataEntityOperationException;
import com.smartalarmclock.Model.Exceptions.PhoneContactDoesNotExistException;
import com.smartalarmclock.Model.Exceptions.ReferenceCannotBeNullException;

final class LogEntry extends DataEntity implements ILogEntry {

	// Database constants
	
	public static final String LogTableName = "Log";
	
	private static final String c_LogColumnNameTimestamp = "Timestamp";
	private static final String c_LogColumnNameContactId = "ContactId";
	private static final String c_LogColumnNameContactName = "ContactName";
	private static final String c_LogColumnNamePhoneEvent = "PhoneEvent";
	private static final String c_LogColumnNameAnsweringMachine = "AnsweringMachine";
	private static final String c_LogColumnNameSentSMSMessage = "SentSMS";
	private static final String c_LogColumnNameRulesItemId = "RulesItemId";
	private static final String c_LogColumnNameRulesItemName = "RulesItemName";
	
	private static final String[] c_QueryAllColumns = new String[] {
		c_ColumnNameId, c_LogColumnNameTimestamp, c_LogColumnNameContactId, c_LogColumnNameContactName,
		c_LogColumnNamePhoneEvent, c_LogColumnNameAnsweringMachine, c_LogColumnNameSentSMSMessage,
		c_LogColumnNameRulesItemId, c_LogColumnNameRulesItemName };
	
	// Members
	
	private Date m_Timestamp;
	private String m_ContactName;
	private PhoneEvent m_Event;
	private boolean m_WentToAnsweringMachine;
	private String m_RulesItemName;
	private String m_SentSMSString;
	private static SingleReference<ISingleContact> m_SingleContactReference;
	private static SingleReference<IRulesItem> m_RulesItemReference;
	
	// Constructors
	
	/**
	 * Creates a new log entry in the log using a SingleContact
	 * @param event the phone event (incoming call or SMS)
	 * @param sentToAnsweringMachine whether the call was sent to the answering machine
	 * @param contact the single contact in the event
	 * @param rulesItem the rules item which handled the event
	 * @param sentSMS the SMS sent automatically
	 */
	protected LogEntry(
		PhoneEvent event,
		boolean sentToAnsweringMachine,
		ISingleContact contact,
		IRulesItem rulesItem,
		String sentSMS) {
		m_SingleContactReference = new SingleReference<ISingleContact>(this, contact, true, SingleContact.class, DeleteMethod.NoAction);
		m_RulesItemReference = new SingleReference<IRulesItem>(this, rulesItem, true, RulesItem.class, DeleteMethod.NoAction);
		Initialize(null, event, sentToAnsweringMachine, null, null, sentSMS);
	}
	
	/**
	 * Creates a new log entry in the log using a phone number
	 * @param event the phone event (incoming call or SMS)
	 * @param sentToAnsweringMachine whether the call was sent to the answering machine
	 * @param phoneNumber the phone number of the event
	 * @param rulesItem the rules item which handled the event
	 * @param sentSMS the SMS sent automatically
	 */
	protected LogEntry(
		PhoneEvent event,
		boolean sentToAnsweringMachine,
		String phoneNumber,
		IRulesItem rulesItem,
		String sentSMS) {
		m_SingleContactReference = new SingleReference<ISingleContact>(this, SingleContact.class, DeleteMethod.NoAction);
		m_RulesItemReference = new SingleReference<IRulesItem>(this, rulesItem, true, RulesItem.class, DeleteMethod.NoAction);
		Initialize(null, event, sentToAnsweringMachine, phoneNumber, null, sentSMS);
	}
	
	/**
	 * Creates a new log entry in the log using a SingleContact
	 * @param id the id of the log entry
	 * @param timestamp the timestamp
	 * @param event the phone event (incoming call or SMS)
	 * @param sentToAnsweringMachine whether the call was sent to the answering machine
	 * @param contact the single contact in the event
	 * @param rulesItem the rules item which handled the event
	 * @param sentSMS the SMS sent automatically
	 */
	private LogEntry(
		int id,
		Date timestamp,
		PhoneEvent event,
		boolean sentToAnsweringMachine,
		ISingleContact contact,
		IRulesItem rulesItem,
		String sentSMS) {
		super(id);
		m_SingleContactReference = new SingleReference<ISingleContact>(this, contact, true, SingleContact.class, DeleteMethod.NoAction);
		m_RulesItemReference = new SingleReference<IRulesItem>(this, rulesItem, true, RulesItem.class, DeleteMethod.NoAction);
		Initialize(timestamp, event, sentToAnsweringMachine, null, null, sentSMS);
	}
	
	/**
	 * Creates a new log entry in the log using a phone number
	 * @param id the id of the log entry
	 * @param timestamp the timestamp
	 * @param event the phone event (incoming call or SMS)
	 * @param sentToAnsweringMachine whether the call was sent to the answering machine
	 * @param phoneNumber the phone number of the event
	 * @param rulesItem the rules item which handled the event
	 * @param sentSMS the SMS sent automatically
	 */
	private LogEntry(
		int id,
		Date timestamp,
		PhoneEvent event,
		boolean sentToAnsweringMachine,
		String phoneNumber,
		IRulesItem rulesItem,
		String sentSMS) {
		super(id);
		m_SingleContactReference = new SingleReference<ISingleContact>(this, SingleContact.class, DeleteMethod.NoAction);
		m_RulesItemReference = new SingleReference<IRulesItem>(this, rulesItem, true, RulesItem.class, DeleteMethod.NoAction);
		Initialize(timestamp, event, sentToAnsweringMachine, phoneNumber, null, sentSMS);
	}
	
	/**
	 * Creates a new log entry in the log using a SingleContact
	 * @param id the id of the log entry
	 * @param timestamp the timestamp
	 * @param event the phone event (incoming call or SMS)
	 * @param sentToAnsweringMachine whether the call was sent to the answering machine
	 * @param contact the single contact in the event
	 * @param rulesItemName the rules item name which handled the event
	 * @param sentSMS the SMS sent automatically
	 */
	private LogEntry(
		int id,
		Date timestamp,
		PhoneEvent event,
		boolean sentToAnsweringMachine,
		ISingleContact contact,
		String rulesItemName,
		String sentSMS) {
		super(id);
		m_SingleContactReference = new SingleReference<ISingleContact>(this, contact, true, SingleContact.class, DeleteMethod.NoAction);
		m_RulesItemReference = new SingleReference<IRulesItem>(this, RulesItem.class, DeleteMethod.NoAction);
		Initialize(timestamp, event, sentToAnsweringMachine, null, rulesItemName, sentSMS);
	}
	
	/**
	 * Creates a new log entry in the log using a phone number
	 * @param id the id of the log entry
	 * @param timestamp the timestamp
	 * @param event the phone event (incoming call or SMS)
	 * @param sentToAnsweringMachine whether the call was sent to the answering machine
	 * @param phoneNumber the phone number of the event
	 * @param rulesItemName the rules item name which handled the event
	 * @param sentSMS the SMS sent automatically
	 */
	private LogEntry(
		int id,
		Date timestamp,
		PhoneEvent event,
		boolean sentToAnsweringMachine,
		String phoneNumber,
		String rulesItemName,
		String sentSMS) {
		super(id);
		m_SingleContactReference = new SingleReference<ISingleContact>(this, SingleContact.class, DeleteMethod.NoAction);
		m_RulesItemReference = new SingleReference<IRulesItem>(this, RulesItem.class, DeleteMethod.NoAction);
		Initialize(timestamp, event, sentToAnsweringMachine, phoneNumber, rulesItemName, sentSMS);
	}
	
	
	// ILogEntry implementation

	@Override
	public ISingleContact GetSingleContact() {
		return m_SingleContactReference.GetReference();
	}
	
	@Override
	public String GetContactName(Context context) {
		ISingleContact contact = m_SingleContactReference.GetReference();
		if (contact != null) {
			try {
				return contact.GetPhoneContactName(context);
			} catch (PhoneContactDoesNotExistException e) { }
		}
		
		return m_ContactName;
	}

	@Override
	public Date GetTimestamp() {
		return m_Timestamp;
	}

	@Override
	public PhoneEvent GetEvent() {
		return m_Event;
	}

	@Override
	public boolean DidGoToAnsweringMachine() {
		return m_WentToAnsweringMachine;
	}

	@Override
	public String GetSentSMSMessageString() {
		return m_SentSMSString;
	}

	@Override
	public IRulesItem GetHandlingRulesItem() {
		return m_RulesItemReference.GetReference();
	}
	
	@Override
	public String GetHandlingRulesItemName(Context context) {
		IRulesItem rulesItem = m_RulesItemReference.GetReference();
		if (rulesItem != null) {
			if (IVIP.class.isInstance(rulesItem)) {
				try {
					return String.format("VIP_%s", ((IVIP)rulesItem).GetContact().GetName(context));
				} catch (PhoneContactDoesNotExistException e) { }
			}
			else {
				return String.format("Profile_%s", ((IProfile)rulesItem).GetName());
			}
		}
		
		return m_RulesItemName;
	}
	
	// Public Methods
	
	/**
	 * @return the CREATE TABLE statement of the Log table
	 */
	static String GetSqlStatementCreateLogTable() {
		return "CREATE TABLE " + LogTableName + " (" +
			c_ColumnNameId + 					" INTEGER PRIMARY KEY, " +
			c_LogColumnNamePhoneEvent + 		" INTEGER NOT NULL, " +
			c_LogColumnNameTimestamp +			" TEXT NOT NULL, " +
			c_LogColumnNameContactId +			" INTEGER, " +
			c_LogColumnNameContactName +		" TEXT, " +
			c_LogColumnNameRulesItemId +		" INTEGER, " +
			c_LogColumnNameRulesItemName +		" TEXT, " +
			c_LogColumnNameAnsweringMachine +	" INTEGER NOT NULL, " +
			c_LogColumnNameSentSMSMessage +		" TEXT, " +
			"FOREIGN KEY(" + c_LogColumnNameContactId + ") REFERENCES " + SingleContact.ContactTableName + "(" + c_ColumnNameId + "), " +
			"FOREIGN KEY(" + c_LogColumnNameRulesItemId + ") REFERENCES " + RulesItem.RulesItemTableName + "(" + c_ColumnNameId + "))";
	}
	
	/**
	 * @return the DROP TABLE statement of the Log table
	 */
	static String GetSqlStatementDropLogTable() {
		return "DROP TABLE IF EXISTS " + LogTableName;
	}
	
	/**
	 * Queries the DB for a log entry by id
	 * @param context the context of the request
	 * @param id the id to query for
	 * @return a matching LogEntry instance
	 * @throws DataEntityDoesNotExistException if the data entity does not exist.
	 */
	static LogEntry GetLogEntryById(Context context, int id) throws DataEntityDoesNotExistException {
		return GetSingleLogEntry(context, c_ColumnNameId + "=" + id);
	}
	
	/**
	 * Queries the DB for a log entry by id
	 * @param context the context of the request
	 * @param id the id to query for
	 * @return a matching LogEntry instance or null if the entity does not exist
	 */
	static LogEntry TryGetLogEntryById(Context context, int id) {
		try {
			return GetLogEntryById(context, id);
		} catch (DataEntityDoesNotExistException e) {
			return null;
		}
	}
	
	/**
	 * Queries the DB for all log entries by single contact
	 * @param context the context of the request
	 * @param contact the contact to query for
	 * @return all matching LogEntry instances
	 */
	static Set<ILogEntry> GetLogEntriesBySingleContact(Context context, ISingleContact contact) {
		return GetLogEntries(context, c_LogColumnNameContactId + "=" + contact.GetId());
	}
	
	/**
	 * Queries the DB for all log entries by the handling rules item
	 * @param context the context of the request
	 * @param rulesItem the handling rules item
	 * @return all matching LogEntry instances
	 */
	static Set<ILogEntry> GetLogEntriesByRulesItem(Context context, IRulesItem rulesItem) {
		return GetLogEntries(context, c_LogColumnNameRulesItemId + "=" + rulesItem.GetId());
	}
	
	/**
	 * Queries the DB for all log entries in the given time range
	 * @param context the context of the request
	 * @param startTime the start time of the time range
	 * @param endTime the end time of the time range
	 * @return all matching LogEntry instances
	 */
	static Set<ILogEntry> GetLogEntriesByTimeRange(Context context, Date startTime, Date endTime) {
		return GetLogEntries(
			context,
			startTime.getTime() + "<=" + c_LogColumnNameTimestamp + " AND " + c_LogColumnNameTimestamp + "<=" + endTime.getTime());
	}
	
	// Private Methods
	
	/**
	 * Queries a single log entry from the DB.
	 * @param context the context of the request (the request Activity)
	 * @param whereClause the where clause for the query
	 * @return the matching single log entry
	 * @throws DataEntityDoesNotExistException if the matching entity does not exist
	 */
	private static LogEntry GetSingleLogEntry(Context context, String whereClause) throws DataEntityDoesNotExistException {
		SQLiteDatabase db = OpenDBForReading(context);
		Cursor cursor = db.query(
			LogTableName,
			c_QueryAllColumns,
			whereClause,
			null, null, null, null);
		
		try {
			if (!cursor.moveToFirst()) {
				throw new DataEntityDoesNotExistException(LogEntry.class, whereClause);
			}
			
			return CreateLogEntryFromCursor(context, cursor);
		}
		finally {
			cursor.close();
			db.close();
		}
	}
	
	/**
	 * Queries the DB using the given where clause
	 * @param context the context of the request
	 * @param whereClause the where clause to use
	 * @return a collection of matching log entries
	 */
	private static Set<ILogEntry> GetLogEntries(Context context, String whereClause) {
		SQLiteDatabase db = OpenDBForReading(context);
		Cursor cursor = db.query(
			LogTableName,
			c_QueryAllColumns,
			whereClause,
			null, null, null, null);
		
		Set<ILogEntry> logEntries = new HashSet<ILogEntry>();
		while (cursor.moveToNext()) {
			logEntries.add(CreateLogEntryFromCursor(context, cursor));
		}
		
		cursor.close();
		db.close();
		return logEntries;
	}
	
	/**
	 * Creates a new SMSMessage instance from the values in the cursor 
	 * @param context the context of the request
	 * @param cursor the cursor holding the values.
	 * @return the matching SMSMessage instance.
	 */
	private static LogEntry CreateLogEntryFromCursor(Context context, Cursor cursor) {
		int id = cursor.getInt(cursor.getColumnIndex(c_ColumnNameId));
		Date timestamp = new Date(Long.parseLong(cursor.getString(cursor.getColumnIndex(c_LogColumnNameTimestamp))));
		PhoneEvent event = PhoneEvent.FromValue(cursor.getInt(cursor.getColumnIndex(c_LogColumnNamePhoneEvent)));
		boolean wentToAnsweringMachine = (cursor.getInt(cursor.getColumnIndex(c_LogColumnNameAnsweringMachine)) != 0);
		String smsSent = cursor.getString(cursor.getColumnIndex(c_LogColumnNameSentSMSMessage));
		
		ContactFactory contactFactory = new ContactFactory();
		ISingleContact contact = null;
		String contactName = null;
		try {
			contact = contactFactory.GetSingleContactById(context, cursor.getInt(cursor.getColumnIndex(c_LogColumnNameContactId)));
		} catch (Exception e) {
			contactName = cursor.getString(cursor.getColumnIndex(c_LogColumnNameContactName));
		}
		
		RulesItemFactory rulesItemFactory = new RulesItemFactory();
		IRulesItem rulesItem = null;
		String rulesItemName = null;
		try {
			int rulesItemId = cursor.getInt(cursor.getColumnIndex(c_LogColumnNameRulesItemId));
			rulesItem = rulesItemFactory.GetRulesItemById(context, rulesItemId);
		} catch (Exception e) {
			contactName = cursor.getString(cursor.getColumnIndex(c_LogColumnNameRulesItemName));
		}
		
		if (contact != null && rulesItem != null) {
			return new LogEntry(id, timestamp, event, wentToAnsweringMachine, contact, rulesItem, smsSent);
		}
		if (contact == null && rulesItem != null) {
			return new LogEntry(id, timestamp, event, wentToAnsweringMachine, contactName, rulesItem, smsSent);
		}
		if (contact != null && rulesItem == null) {
			return new LogEntry(id, timestamp, event, wentToAnsweringMachine, contact, rulesItemName, smsSent);
		}
		return new LogEntry(id, timestamp, event, wentToAnsweringMachine, contactName, rulesItemName, smsSent);
	}
	
	/**
	 * Initializes a new instance
	 * @param event the phone event
	 * @param sentToAnsweringMachine whether the call went to the answering machine
	 * @param contactName the contact name
	 * @param rulesItemName the name of the rules item
	 * @param sentSMS the SMS sent automatically
	 */
	private void Initialize(Date timestamp, PhoneEvent event, boolean sentToAnsweringMachine, String contactName, String rulesItemName, String sentSMS) {
		m_Timestamp = timestamp;
		if (m_Timestamp == null) {
			// Getting current time
			m_Timestamp = Calendar.getInstance().getTime();
		}
		
		m_Event = event;
		m_WentToAnsweringMachine = sentToAnsweringMachine;
		
		m_ContactName = contactName;
		m_RulesItemName = rulesItemName;
		m_SentSMSString = sentSMS;
	}
	
	// IDataEntity implementation

	@Override
	public void Refresh(Context context) throws DataEntityDoesNotExistException {
	}
	
	@Override
	public void Save(Context context) throws InvalidSaveDataEntityOperationException {
		m_RulesItemReference.SaveReferences(context);
		m_SingleContactReference.SaveReferences(context);
		
		super.Save(context);
	}
	
	@Override
	public void Delete(Context context) throws InvalidDeleteDataEntityOperationException, InvalidSaveDataEntityOperationException {
		super.Delete(context);
		
		m_RulesItemReference.OnDeleteReferencingEntity(context);
		m_SingleContactReference.OnDeleteReferencingEntity(context);
	}
	
	// IReferencingDataEntity implementation
	
	@Override
	public boolean IsReferenced(Context context, IReferencedDataEntity referencedEntity) {
		return m_RulesItemReference.GetReference() == referencedEntity ||
			m_SingleContactReference.GetReference() == referencedEntity;
	}

	@Override
	public boolean CanRemoveReference(Context context, IReferencedDataEntity referencedEntity) {
		// Both single contact and rules item references can be removed.
		return IsReferenced(context, referencedEntity);
	}

	@Override
	public void RemoveReference(Context context, IReferencedDataEntity referencedEntity) throws InvalidNullReferenceOnDeleteException, InvalidReferenceToRemoveException {
		if (!IsReferenced(context, referencedEntity)) {
			throw new InvalidReferenceToRemoveException(LogEntry.class, GetId(), referencedEntity.getClass(), referencedEntity.GetId());
		}
		
		try {
			if (m_RulesItemReference.GetReference() == referencedEntity) {
				// Saving the rules item name instead
				m_RulesItemName = GetHandlingRulesItemName(context);
				m_RulesItemReference.Set(null);
			}
			else
			{
				// Saving the contact name instead
				m_ContactName = GetContactName(context);
				m_SingleContactReference.Set(null);
			}
			
			SetUpdated();
		} catch (ReferenceCannotBeNullException e) {
			// Cannot happen since both are set to nullable
		}
	}
	
	// DataEntity implementation

	@Override
	protected Hashtable<String, ContentValues> GetRecordValues() {
		ContentValues values = new ContentValues();
		
		values.put(c_LogColumnNameTimestamp, m_Timestamp.getTime());
		values.put(c_LogColumnNamePhoneEvent, m_Event.GetValue());
		values.put(c_LogColumnNameAnsweringMachine, m_WentToAnsweringMachine);
		
		if (m_SentSMSString != "") {
			values.put(c_LogColumnNameSentSMSMessage, m_SentSMSString);
		}
		
		ISingleContact contact = m_SingleContactReference.GetReference();
		if (contact != null) {
			values.put(c_LogColumnNameContactId, contact.GetId());
		}
		else
		{
			values.put(c_LogColumnNameContactName, m_ContactName);
		}
		
		IRulesItem rulesItem = m_RulesItemReference.GetReference();
		if (rulesItem != null) {
			values.put(c_LogColumnNameRulesItemId, rulesItem.GetId());
		}
		else
		{
			values.put(c_LogColumnNameRulesItemName, m_RulesItemName);
		}
		
		Hashtable<String, ContentValues> valuesTable = new Hashtable<String, ContentValues>();
		valuesTable.put(LogTableName, values);
		return valuesTable;
	}

	@Override
	protected Set<String> GetTableNames() {
		HashSet<String> tableNames = new HashSet<String>();
		tableNames.add(LogTableName);
		return tableNames;
	}
}
